'''
Created on Nov 25, 2009

Module with fucntions that take an input, byte array, and (de)compress it
according to the application and form descriptors.

@author: paul
'''
import array
import math
import logging
from schema_reader import FormHandler
from schema_reader import AppHandler

_LOGGER = logging.getLogger("mx.org.came.codec")
    
def decode(application_dictionary, form_dictionary, query_byte_array):
    """Given a dictionary with the structure of the application descriptor
    (the XML file that describes the database and tables to be used),
    a dictionary with the structure of the form descriptor (the XML file
    that describes a query on the tables described in the application
    descriptor) and a byte array that specifies the ID of the application
    descriptor in the first byte, the ID of the form descriptor on the
    second one and the values of the arguments for the query in the rest.
    """
    _LOGGER.debug("Entering decode")
    _LOGGER.debug("application_dictionary: %s", application_dictionary)
    _LOGGER.debug("form_dictionary: %s", form_dictionary)
    _LOGGER.debug("query_byte_array: %s", query_byte_array)
    data = {}
    data['dbName'] = application_dictionary['name']
    cur_bit_pos = 0
    is_fetch_op = False
    if form_dictionary['operation'] == str(FormHandler.OP_SEARCH_ID) \
            or \
            form_dictionary['operation'] == str(FormHandler.OP_ADV_SEARCH_ID):
        is_fetch_op = True
    form_table_dict = form_dictionary['schemas']
    for sid in form_table_dict.iterkeys():
        app_schema_dict = application_dictionary['schemas'][sid]
        table_list = form_table_dict[sid]['fields']
        op_dict = form_table_dict[sid]['operator']
        for field_id in table_list:
            r = int(app_schema_dict[field_id]['range'])
            if r == AppHandler.DATE_FIELD_RANGE : 
                r = 64 # date
                data[app_schema_dict[field_id]['name']] = \
                        get_int(query_byte_array
                               , cur_bit_pos
                               , r)
                cur_bit_pos += r
            elif r == AppHandler.STR_FIELD_RANGE: # str
                str_len = get_int(query_byte_array, cur_bit_pos, 8)
                cur_bit_pos += 8
                str_array = array.array('B')
                while str_len != 0:
                    str_len -= 1
                    str_array.append(get_int(query_byte_array, cur_bit_pos, 8))
                    cur_bit_pos += 8
                data[app_schema_dict[field_id]['name']] = str_array.tostring()
            elif r == AppHandler.MC_FIELD_RANGE:
                r = len(app_schema_dict[field_id]['choice'])
                select = get_int(query_byte_array, cur_bit_pos, r)
                data[app_schema_dict[field_id]['name']] = select
                cur_bit_pos += r
            elif r == AppHandler.SC_FIELD_RANGE:
                r = int(math.ceil(math.log(len(app_schema_dict[field_id]['choice'])
                                           , 2)))
                select = get_int(query_byte_array, cur_bit_pos, r)
                data[app_schema_dict[field_id]['name']] = select
                cur_bit_pos += r
            else:
                data[app_schema_dict[field_id]['name']] = get_int(query_byte_array
                                                              , cur_bit_pos
                                                              , r)
                cur_bit_pos += r
            if is_fetch_op:
                tmp = data[app_schema_dict[field_id]['name']]
                if op_dict[field_id] == 'Equal to' : op = "="
                elif op_dict[field_id] == 'Less than': op = "<"
                else: op = ">"
                data[app_schema_dict[field_id]['name']] = '{0}{1}'.format(op, tmp)
                
        data['schemaName'] = application_dictionary['schemas'][sid]['name']
        if is_fetch_op:
            data['logical'] = form_dictionary['logical']
    return data
    
    
def encode_fetch_result(result_list, application_dictionary, form_dictionary):
    """
    This function will compressed the result which is returned
    by SQL server then send to client
    """
    _LOGGER.debug("Entering encode_fetch_result")
    _LOGGER.debug("application_dictionary: %s", application_dictionary)
    _LOGGER.debug("form_dictionary: %s", form_dictionary)
    _LOGGER.debug("result_list: %s", result_list)
    num = 0
    form_table_dict = form_dictionary['schemas']
    final_array = array.array('B')
    for result in result_list:
        r_id, size = 0, 0
        for sid in form_table_dict.iterkeys():
            app_schema_dict = application_dictionary['schemas'][sid]
            table_list = form_table_dict[sid]['fetch']
            new_list = []
            for field_id in table_list:
                ranges = int(app_schema_dict[field_id]['range'])
                if ranges == AppHandler.DATE_FIELD_RANGE : 
                    ranges = 16 # date
                    size += ranges
                    new_list.append([ranges, result[r_id]])
                    r_id += 1
                elif ranges == AppHandler.STR_FIELD_RANGE: # str
                    size += 8 + 8 * len(result[r_id])
                    new_list.append([ranges, result[r_id]])
                    r_id += 1
                elif ranges == AppHandler.MC_FIELD_RANGE:
                    ranges = len(app_schema_dict[field_id]['choice'])
                    size += ranges
                    new_list.append([ranges, result[r_id]])
                    r_id += 1
                elif ranges == AppHandler.SC_FIELD_RANGE:
                    ranges = int(math.ceil(math.log(len(
                            app_schema_dict[field_id]['choice']), 2)))
                    size += ranges
                    new_list.append([ranges, result[r_id]])
                    r_id += 1
                else:
                    size += ranges
                    new_list.append([ranges, result[r_id]])
                    r_id += 1
        byte_list = []
        s = (size / 8) + 1 if size % 8 else size / 8
        for i in range(s):
            byte_list.append(0)
        size = 0
        ret_array = array.array('B', byte_list)
        for w_list in new_list:
            _LOGGER.debug(w_list)
            num += 1
            if (w_list[0] == AppHandler.STR_FIELD_RANGE):
                ret_array = set_bits(ret_array, size, 8, len(w_list[1]))
                size += 8
                str_array = array.array('B', w_list[1])
                for ch in str_array:
                    ret_array = set_bits(ret_array, size, 8, ch)
                    size += 8
            else:
                ret_array = set_bits(ret_array, size, w_list[0], w_list[1])
                size += w_list[0]
        final_array.extend(ret_array)
    _LOGGER.debug('Total Number of recs: ' + str(num))
    mask = 0x000000FF
    final_array.insert(0, num & mask)
    final_array.insert(0, (num >> 8) & mask)
    final_array.insert(0, (num >> 16) & mask)
    final_array.insert(0, (num >> 24) & mask)
    _LOGGER.debug("encode_fetch_result return value: %s", final_array)
    return final_array

def get_int(query_byte_array, current_bit_position, len_bits):
    """
    This a basic function for decompressed the data.
    This function will return an intenger given a 
    byte stream, query_byte_array, read a certain length, len_bits,
    form a point, current_bit_position. 
    """

    # Length of PID is 17 bits
    # means the first two bytes plus the first bits of third byte
    start_bit_pos = current_bit_position % 8
    cur_byte_pos = current_bit_position / 8
    if current_bit_position == 0: cur_byte_pos = 0 
    pid, cur_working_bits = 0, 0
    len_of_working_bytes = (start_bit_pos + len_bits) / 8 + 1
    # Get require bytes
    working_byte_array = query_byte_array[cur_byte_pos:cur_byte_pos \
                                        + len_of_working_bytes]
    #print working_byte_array
    for cur_byte_ary in working_byte_array :
        mask = 0x80
        while cur_working_bits < len_bits:
            if start_bit_pos != 0:
                start_bit_pos -= 1
            elif cur_byte_ary & mask: 
                pid += 1
                pid <<= 1
                cur_working_bits += 1
            else :
                cur_working_bits += 1
                pid <<= 1
            mask >>= 1
            if mask == 0 : break
    return pid >> 1    

def set_bits(ret_array, current_bit_position, len_bits, val):
    """Writes the bits in ``val`` to ``ret_array`` starting at
    ``current_bit_position``, filling with zeros to the left
    till ``len_bits`` is reached.
    """
    val_list = get_val_list(val, len_bits)
    for v in val_list:
        start_bit_pos = current_bit_position % 8
        cur_byte_pos = current_bit_position / 8
        mask = 0x1
        #// adjust the mask.
        mask <<= (7 - start_bit_pos)
        if v == '1':
            #// set the bit at byteNum/offset
            ret_array[cur_byte_pos] = ret_array[cur_byte_pos] | mask
        else:
            #// clear the bit at byteNum/offset
            ret_array[cur_byte_pos] = ret_array[cur_byte_pos] & (~mask)
        #// increment the location pointer
        current_bit_position += 1
    return ret_array

def get_val_list(val, len_bits):
    ret_list = []
    tmp = bin(val)
    tmp = tmp.lstrip('0b')
    kk = array.array('c', tmp)
    for i in range(len(kk)):
        ret_list.append(kk[i])
    _LOGGER.debug(ret_list)
    while len(ret_list) < len_bits:
        ret_list.insert(0, '0')
    _LOGGER.debug(ret_list)
    return ret_list
